/*
 * Albareth - an action roleplaying game.
 * Copyright (C) 2008 Jens Anuth
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 *   02110-1301, USA.
 * 
 * $Id: PlayerCharacter.java 49 2008-05-17 02:35:15Z jens464 $
*/
package albareth.core.entity;

import java.util.Iterator;

import albareth.core.Game;
import albareth.core.GameEffect;
import albareth.core.GameMap;
import albareth.core.action.Spell;
import albareth.core.brain.Brain;
import albareth.frontend.Frontend;
import albareth.frontend.Resources;
import albareth.frontend.Tile;
import albareth.util.BresenhamLine;
import frosch.util.FToolkit;
import frosch.util.glider.Easing;

public class PlayerCharacter extends Mobile
{
    public static final int SLOT_NONE     = -1;
    public static final int SLOT_HELMET   = 0;
    public static final int SLOT_ARMOR    = 1;
    public static final int SLOT_WEAPON   = 2;
    public static final int SLOT_AMULETTE = 3;
    public static final int SLOT_RING     = 4;
    public static final int SLOT_SHIELD   = 5;
    public static final int NUM_SLOTS     = 6;

    /**
     * Indicates whether to use symmetric or simple Bresenham ray-casting
     * for FoV-calculation. Symmetric yields better results but is
     * more expensive to calculate.
     */  
    public static final boolean SYMMETRIC_BRESENHAM_FOV = true;
    
    /**
     * Experience needed for next level.
     */
    private static int[] _experienceTable = new int[]{
        0, 75, 150, 275, 500, 800, 1300, 2100, 3500
    };
    
    Frontend _frontend;

    private boolean[][] _fieldOfVision;

    private int _str = 0;
    private int _dex = 0;
    private int _int = 0;
    private int _experience = 0;
    private int _gold = 0;
    
    private EquipableItem[] _equipment = new EquipableItem[6];
    private EquipmentChangeListener _equipChangedListener;
    



    public PlayerCharacter(String name, Tile tile)
    {
        this(name, tile, 0, 0);
    }
    
    public PlayerCharacter(String name, Tile tile, int hits, int mana)
    {
        super(name, tile, hits, mana);
        setBaseMeleeAttackSpeed(UNARMED_ATTACK_SPEED);
        setEvil(false);
    }
    
    public boolean tryEquip(EquipableItem item)
    {
        if (item.getEquipSlot() == PlayerCharacter.SLOT_NONE) {
            return false; //not equipable
        }
        
        if (item.isLockedDown()) {
            return false; //item is glued to where it is.
        }
        
        if (_equipment[item.getEquipSlot()] != null) {
            return false; //slot already occupied
        }
        
        if (item.getLocation() != null && 
                item.getLocation().distance(getLocation()) > 1) {
            return false; //item too far away.
        }
        
        boolean success = item.equip(this);
        if (!success) {
            return false; //item refused to be equipped by this mobile.
        }
        
        item.remove();
        item.setOwner(this);
        _equipment[item.getEquipSlot()] = item;
        notifyEquipmentChanged();
        return true;
    }
    
    
    public boolean tryUnequip(EquipableItem item)
    {
        if (_equipment[item.getEquipSlot()] != item) {
            return false; //item not equipped at all
        }

        if (item.isLockedDown()) {
            return false; //item is glued to where it is.
        }
        
        boolean success = item.unequip(this);
        if (!success) {
            return false; //item refused to be unequipped.
        }
        
        _equipment[item.getEquipSlot()] = null;
        item.remove();
        item.setOwner(null);
        notifyEquipmentChanged();
        return true;
    }

    /**
     * Extends superclass by also considering the equipped items. 
     */
    public boolean removeItem(Item item)
    {
        if (item.isEquipable() && item.getOwner() == this) {
            return tryUnequip((EquipableItem)item);
        }
        return super.removeItem(item);
        /*
        boolean removed = super.removeItem(item);
        if (!removed) {
            return tryUnequip(item);
        }
        return false;
        */
    }


    protected void processMeleeMiss(int hitRoll)
    {
        if (isMeleeCooldownActive()) {
            int dexRoll = FToolkit.rand(1, 20);
            if (dexRoll == 20 || dexRoll > getDex()) {
                activateStumble();
                emitSound(Resources.SOUND_STUMBLING);
            }
        }
    }

    
    public EquipableItem getEquippedItem(int slot)
    {
        return _equipment[slot];
    }

    
    public boolean isPlayerControlled()
    {
        return true;
    }

    public void setFrontend(Frontend frontend)
    {
        _frontend = frontend;
        addItemContainerChangeListener(_frontend);
        addEquipmentChangeListener(_frontend);
    }
    
    public void addEquipmentChangeListener(EquipmentChangeListener listener)
    {
        _equipChangedListener = listener;
    }
    
    private void notifyEquipmentChanged()
    {
        if (_equipChangedListener != null) {
            _equipChangedListener.equipmentChanged(this);
        }
    }

    public Frontend getFrontend()
    {
        return _frontend;
    }
    
    public MobileController getMobileController()
    {
        return _frontend;
    }

    
    public int getStr()
    {
        return _str;
    }

    public void setStr(int str)
    {
        _str = str;
    }

    public int getDex()
    {
        return _dex;
    }

    public void setDex(int dex)
    {
        _dex = dex;
    }

    public int getInt()
    {
        return _int;
    }

    public void setInt(int i)
    {
        _int = i;
    }

    /**
     * The base melee damage plus str-mod.
     */
    public int getBaseMeleeDamageMin()
    {
        int min = super.getBaseMeleeDamageMin();
        return (int)Math.round(min * ((double)getStr()/10) * getSwingRegenPct());
    }

    /**
     * base melee damage plus str-mod.
     */
    public int getBaseMeleeDamageMax()
    {
        int max = super.getBaseMeleeDamageMax();
        return (int)Math.round(max * ((double)getStr()/10) * getSwingRegenPct());
        //return Math.round(max * ((float)getStr()/10));
    }

    /**
     * TODO: include dex-mod.
     */
    public int getBaseArmorClass()
    {
        return super.getBaseArmorClass();
    }

    /**
     * TODO: include dex-mod.
     */
    public int getBaseTHAC0()
    {
        return (int)Math.round(super.getBaseTHAC0() * getSwingRegenPct());
    }
    
    protected boolean hitSucceeded(int roll, Mobile targetMobile)
    {
        return super.hitSucceeded(roll, targetMobile) &&
            Math.random() < getSwingRegenPct();
    }

    
    //TODO: attack speed, base attack speed (determined by dex and by weapon)
    
    private double getSwingRegenPct()
    {
        long meleeCd = getMeleeCooldownTimeLeft();
        if (meleeCd < 0) {
            return 1.0;
        } else {
            return 1d - (double)meleeCd / getBaseMeleeAttackSpeed();
        }
    }

    

    
    
    
    public int getExperience()
    {
        return _experience;
    }

    public void setExperience(int experience)
    {
        _experience = experience;
    }
    
    public int getExperienceForNextLevel()
    {
        return _experienceTable[getLevel()];
    }

    public int getWalkCooldown()
    {
        return 400;
    }
    
    /**
     * Can walk whenever he wishes.
     */
    public boolean isWalkCooldownActive(long currentTime)
    {
        return false;
    }
    
    public void infoMsg(String message)
    {
        _frontend.infoMsg(message);
    }

    public void warnMsg(String message)
    {
        _frontend.warnMsg(message);
    }
    
    public void eventMsg(String message)
    {
        _frontend.eventMsg(message);
    }
    
    public void moveTo(GameMap map, int x, int y)
    {
        super.moveTo(map, x, y);
        calculateCurrentFieldOfVision();
    }
    
    public boolean canMelee()
    {
        return !isStumbling();
    }

    

    public void calculateCurrentFieldOfVision()
    {
        _fieldOfVision = new boolean[2*Game.RANGEOFSIGHT + 1][2*Game.RANGEOFSIGHT + 1];

        //player location itself:
        exploreLocation(getX(), getY(), getMap());

        //cast rays to borders of view range and mark visible:
        for (int i = -Game.RANGEOFSIGHT; i <= Game.RANGEOFSIGHT; i++)
        {
            castViewRay(getX()+i, getY()-Game.RANGEOFSIGHT);
            castViewRay(getX()+i, getY()+Game.RANGEOFSIGHT);
        }
        for (int i = -(Game.RANGEOFSIGHT-1); i < Game.RANGEOFSIGHT; i++)
        {
            castViewRay(getX()-Game.RANGEOFSIGHT, getY()+i);
            castViewRay(getX()+Game.RANGEOFSIGHT, getY()+i);
        }

        //if we want only simple ray-casting, we are done.
        if (SYMMETRIC_BRESENHAM_FOV == false) {
            return;
        }
        
        //for every square in view range, still not visible,
        //cast another bresenham and an inverse one from there
        //and try to reach the view positon.
        GameMap map = getMap();
        BresenhamLine invBresen = new BresenhamLine();
        for (int x = 0; x < 2*Game.RANGEOFSIGHT + 1; x++)
        {
            for (int y = 0; y < 2*Game.RANGEOFSIGHT + 1; y++)
            {
                if (_fieldOfVision[x][y] == true) {
                    continue;
                }
                int mapX = getX() + x - Game.RANGEOFSIGHT;
                int mapY = getY() + y - Game.RANGEOFSIGHT;;
                if (map.isOutOfBounds(mapX, mapY))  {
                    continue;
                }

                boolean seesMe = true;
                invBresen.plot(mapX, mapY, getX(), getY());
                while (invBresen.next())
                {
                    if (!map.isTranslucent(invBresen.getX(), invBresen.getY())) {
                        seesMe = false;
                        break;
                    }
                }
                if (!seesMe)
                {
                    seesMe = true;
                    invBresen.plot(getX(), getY(), mapX, mapY);
                    while (invBresen.next())
                    {
                        if (!map.isTranslucent(invBresen.getX(), invBresen.getY())) {
                            seesMe = false;
                            break;
                        }
                    }

                }
                
                if (seesMe) {
                    //System.err.println("piep");
                    exploreLocation(mapX, mapY, map);
                }
            }
        }


    }

    /**
     * calculates los and explores between player position and x1,y1.
     */
    private boolean castViewRay(int x1, int y1)
    {
        GameMap map = getMap();

        BresenhamLine bresen = new BresenhamLine();
        bresen.plot(getX(), getY(), x1, y1);
        while (bresen.next())
        {
            exploreLocation(bresen.getX(), bresen.getY(), map);
            if (!map.isTranslucent(bresen.getX(), bresen.getY())) {
                return false;
            }
        };

        return true;
    }

    private void exploreLocation(int x, int y, GameMap map)
    {
        //set to explored:
        map.setExplored(x, y, true);
        //set currently visible:
        _fieldOfVision[x - getX() + Game.RANGEOFSIGHT][y - getY() + Game.RANGEOFSIGHT] = true;
        

        //trigger spotting on discovered mobile
        Mobile mobile = map.getMobile(x, y);
        if (mobile != null && mobile.getBrain() != null) {
            mobile.getBrain().notifySpot(this);
            
            if (getLocation().distance(mobile.getLocation()) <
                    Brain.APPROACH_DISTANCE)
            {
                mobile.getBrain().notifyApproached(this);
            }
        }

    }

    public boolean hasLineOfSight(int x, int y, GameMap map)
    {
        GameMap thisMap = getMap();
        
        if (map != thisMap) {
            return false;
        }
        
        int visMapX = x - getX() + Game.RANGEOFSIGHT;
        int visMapY = y - getY() + Game.RANGEOFSIGHT;
        if (visMapX < 0 || visMapY < 0 || visMapX >= _fieldOfVision.length ||
                visMapY >= _fieldOfVision[0].length)
        {
            return false;
        }
        return checkFieldOfVision(visMapX, visMapY);
    }
    
    public boolean checkFieldOfVision(int visMapX, int visMapY)
    {
        return _fieldOfVision[visMapX][visMapY];
    }

    public void addExperience(int granted)
    {
        Easing easing;
        switch (FToolkit.rand(4))
        {
            case 0:
                easing = Easing.EASING_QUADRATIC;
                break;
            case 1:
                easing = Easing.EASING_QUINTIC;
                break;
            case 2:
                easing = Easing.EASING_QUARTIC;
                break;
            default:
                easing = Easing.EASING_CUBIC;
        }
        _experience += granted;
        emitOverheadMessage(
                String.valueOf("+" + granted + "XP"),
                Resources.COLOR_BLUE,
                easing
        );
        
        if (_experience >= getExperienceForNextLevel())
        {
            emitOverheadMessage(
                    String.valueOf("LEVEL UP!"),
                    Resources.COLOR_WHITE,
                    Easing.EASING_ELASTIC
            );
            emitEffect(GameEffect.entityBound(Resources.EFFECT_LEVEL_UP, this, -1));
            emitSound(Resources.SOUND_LEVEL_UP);

            levelUp();
        }
    }
    
    public void levelUp()
    {
        if (getLevel() >= (_experienceTable.length-1)) {
            return; //max level reached
        }
        
        setLevel(getLevel()+1);
        
        if (Math.random() > .5) {
            setStr(getStr()+1);
        }
        if (Math.random() > .5) {
            setDex(getDex()+1);
        }
        if (Math.random() > .5) {
            setInt(getInt()+1);
        }
        
        setMaxHitpoints(getMaxHitpoints() + getStr());
        setHitpoints(getMaxHitpoints());
        
        setMaxMana(getMaxMana() + getInt());
        setMana(getMaxMana());
        
        setBaseTHAC0(getBaseTHAC0()-2);
        
        for (Iterator it = getSpells().iterator(); it.hasNext(); ) {
            Spell spell = (Spell)it.next();
            spell.setSpellStrength(spell.getSpellStrength()+1);
        }
        

    }
    
    /**
     * Player has killed someone.
     */
    protected void onKill(Mobile victim)
    {
            addExperience(getXPGranted());
    }

    public int getGold()
    {
        return _gold;
    }

    public void addGold(int amount)
    {
        _gold += amount;
    }
    
    public void consumeGold(int amount)
    {
        _gold -=amount;
    }


    public int regenerateHitpoints(int hits)
    {
        int regged = super.regenerateHitpoints(hits);
        if (regged > 0) {
            emitOverheadMessage(
                    "+" + regged + " LIFE",
                    Resources.COLOR_GREEN, Easing.EASING_QUADRATIC
            );
        }
        return regged;
    }

    public int regenerateMana(int mana)
    {
        int regged = super.regenerateMana(mana);
        if (regged > 0) {
            emitOverheadMessage(
                    "+" + regged + " MANA",
                    Resources.COLOR_GREEN, Easing.EASING_QUADRATIC
            );
        }
        return regged;

    }
    
    public int takeDamage(int rawDamage, int damageType, Entity causer, boolean canCrit)
    {
        int damageTaken = super.takeDamage(rawDamage, damageType, causer, canCrit);
        if (damageTaken > 0) {
            emitSound(Resources.SOUND_STUMBLING);
        }
        return damageTaken;
    }

    protected int onDeath(Entity killer)
    {
        _frontend.playerDied();
        return 0;
    }


}
